#include "block_operations.h"

#include <errno.h>
#include <sys/types.h>
#include <attr/xattr.h>

#include <stdio.h>
#include <stdlib.h>

#include <string>

#include "glog/logging.h"

#include "fletcher32.h"
#include "real_fs_operations.h"
#include "scoped_ptr.h"

using namespace std;

// TODO(awong): Ick. static object. remove.
static const std::string TFFS_ATTR_PREFIX = "user.tffs.";

static inline off_t find_block_start(off_t block_size, off_t at) {
    return (at / block_size) * block_size;
}

static int get_block_no(off_t block_size, off_t start) {
    return start / block_size;
}

static std::string get_block_checksum_attr_name(int block_no) {
    char buf[50];
    snprintf(buf, sizeof(buf), "%x", block_no);
    return TFFS_ATTR_PREFIX + "checksum." + buf;
}

static std::string get_block_checksum_ts_attr_name() {
    return TFFS_ATTR_PREFIX + "checksum.ts";
}

static int read_checksum_attr(int fd, int block_no, uint32_t* checksum) {
    // TODO(awong): We need to know the mtime of the file and compare against
    // checksum timestamp to avoid stale checksums if someone modifies the
    // underlying filesystem.
    int attr_size = fgetxattr(fd, get_block_checksum_attr_name(block_no).c_str(),
                              checksum, sizeof(*checksum));
    if (attr_size < 0) {
        return -errno;
    }
    if (attr_size != sizeof(*checksum)) {
        return -EMSGSIZE;
    }

    return 0;
}

static int write_checksum_attr(int fd, int block_no, uint32_t checksum) {
    int attr_size =
        fsetxattr(fd, get_block_checksum_attr_name(block_no).c_str(), &checksum,
                  sizeof(checksum), 0);
    if (attr_size != 0) {
        return -errno;
    }

    // TODO(awong): Figure out timestamp semantics, and if we care about
    // endianess.
    time_t now = time(NULL);
    attr_size = fsetxattr(fd, get_block_checksum_ts_attr_name().c_str(), &now,
                          sizeof(time_t), 0);
    if (attr_size != 0) {
        return -errno;
    }

    return 0;
}

static int truncate_checksums(int fd, off_t after) {
    // (1) Find number of blocks in file.
    // (2) If we truncate to middle of block, update that block.
    // (3) Erase all following checksums.
    return 0;
}

static int lseek_and_log(int fd, off_t at) {
    off_t cur_offset = lseek(fd, at, SEEK_SET);
    if (cur_offset == -1) {
        int errval = errno;
        LOG(ERROR) << "Seek failed for: " << strerror(errval);
        return -errval;
    }
    if (cur_offset != at) {
        LOG(ERROR) << "Seek went to wrong location.";
        return -ESPIPE;
    }
    return 0;
}

static off_t clobber_block(ssize_t block_size, int fd, off_t at,
                           const char* buf, ssize_t size) {
    int retval = lseek_and_log(fd, at);
    if (0 != (retval = lseek_and_log(fd, at))) {
        return retval;
    }

    uint32_t new_checksum = 0;
    if (size < block_size) {
        scoped_ptr_malloc<char> block((char*)calloc(block_size, sizeof(char)));
        memcpy(block.get(), buf, size);
        new_checksum = fletcher32(block.get(), block_size);
    } else {
        new_checksum = fletcher32(buf, block_size);
    }
    if (0 != (retval = write_checksum_attr(fd, get_block_no(block_size, at),
                                           new_checksum))) {
        LOG(ERROR) << "Failed writing checksum attribute: " << strerror(-retval);
        // We continue since failing the checksum write should make us drop
        // data.
    }

    ssize_t amt_to_write = std::min(size, block_size);
    return at + real_write(fd, buf, amt_to_write);
}

static off_t read_modfy_write_block(size_t block_size, int fd, off_t at,
                                    const char* buf, size_t size) {
    // Proper updating should be:
    //   1) Read the whole block.
    //   2) Verify checksum on block, and recover on failure.
    //   3) In-memory update block.
    //   4) Schedule parity update.
    //   5) Write full block.

    // Allocate space for storing the block.
    scoped_ptr_malloc<char> block((char*)calloc(block_size, sizeof(char)));
    if (block.get() == NULL) {
        LOG(ERROR) << "Unable to allocate " << block_size << " for a block.";
        return -ENOMEM;
    }

    // (1) Read the starting block.
    off_t block_start = find_block_start(block_size, at);
    off_t cur_offset = block_start;
    int seek_result = lseek_and_log(fd, cur_offset);
    if (seek_result != 0) {
        return seek_result;
    }
    ssize_t amt_read = real_read(fd, block.get(), block_size);
    if (amt_read < 0) {
        LOG(ERROR) << "Unable to read block for verification: "
                   << strerror(-amt_read);
        return amt_read;
    }

    // (2) Verify the checksum.
    uint32_t checksum = fletcher32(block.get(), block_size);
    uint32_t stored_checksum;
    int block_no = get_block_no(block_size, block_start);
    // TODO(awong): What should we do if we can't read the checksum.
    if (read_checksum_attr(fd, block_no, &stored_checksum) == 0) {
        if (checksum != stored_checksum) {
            // TODO(ajwong): Block is corrupt. Do recovery here.
        }
    } else {
        VLOG(0) << "No checksum found for block " << block_no;
    }

    // (3) In-memory update block.
    off_t offset_from_start = at - block_start;
    ssize_t to_copy = std::min(size, block_size - offset_from_start);
    memcpy(block.get() + offset_from_start, buf, to_copy);

    // (4) Schedule a parity update.
    
    // (5) Update attributes.

    ssize_t to_write = std::min(size+offset_from_start, block_size);

    return clobber_block(block_size, fd, block_start, block.get(), to_write);
}

off_t update_block(size_t block_size, int fd, off_t at, const char* buf,
                   size_t size) {
    // If we're writing a full aligned block, we do not need to validate the
    // checksum, so pick an easier code path.  Otherwise, we need to do a
    // read-modify-write with validation, and possibly recovery.

    off_t block_start = find_block_start(block_size, at);
    if (block_start == at && size >= block_size) {
        return clobber_block(block_size, fd, at, buf, size);
    } else {
        return read_modfy_write_block(block_size, fd, at, buf, size);
    }
}

